#!/usr/bin/perl
#
# This script lets you update a hierarchy of files in an atomic way by
# first creating a new hierarchy using rsync's --link-dest option, and
# then swapping the hierarchy into place.  **See the usage message for
# more details and some important caveats!**

use strict;
use Cwd 'abs_path';

my $RSYNC_PROG = '/usr/bin/rsync';
my $RM_PROG = '/bin/rm';

my $dest_dir = $ARGV[-1];
usage(1) if $dest_dir eq '' || $dest_dir =~ /^--/;

if (!-d $dest_dir) {
    print STDERR "$dest_dir is not a directory.\n\n";
    usage(1);
}

if (@_ = grep(/^--(link|compare)-dest/, @ARGV)) {
    $_ = join(' or ', @_);
    print STDERR "You may not use $_ as an rsync option.\n\n";
    usage(1);
}

$dest_dir = abs_path($dest_dir);
if ($dest_dir eq '/') {
    print STDERR 'You must not use "/" as the destination directory.', "\n\n";
    usage(1);
}

my $old_dir = "$dest_dir~old~";
my $new_dir = $ARGV[-1] = "$dest_dir~new~";

system($RM_PROG, '-rf', $old_dir) if -d $old_dir;

if (system($RSYNC_PROG, "--link-dest=$dest_dir", @ARGV)) {
    if ($? == -1) {
	print "failed to execute $RSYNC_PROG: $!\n";
    } elsif ($? & 127) {
	printf "child died with signal %d, %s coredump\n",
	    ($? & 127),  ($? & 128) ? 'with' : 'without';
    } else {
	printf "child exited with value %d\n", $? >> 8;
    }
    exit $?;
}

rename($dest_dir, $old_dir) or die "Unable to rename $new_dir to $old_dir: $!";
rename($new_dir, $dest_dir) or die "Unable to rename $new_dir to $dest_dir: $!";

exit;


sub usage
{
    my($ret) = @_;
    my $fh = $ret ? *STDERR : *STDOUT;
    print $fh <<EOT;
Usage: atomic-rsync [RSYNC-OPTIONS] HOST:/SOURCE/DIR/ /DEST/DIR/
       atomic-rsync [RSYNC-OPTIONS] HOST::MOD/DIR/ /DEST/DIR/

This script lets you update a hierarchy of files in an atomic way by first
creating a new hierarchy (using hard-links to leverage the existing files),
and then swapping the new hierarchy into place.  You must be pulling files
to a local directory, and that directory must already exist.  For example:

    atomic-rsync -av host:/remote/files/ /local/files/

This would make the transfer to the directory /local/files~new~ and then
swap out /local/files at the end of the transfer by renaming it to
/local/files~old~ and putting the new directory into its place.  The
/local/files~old~ directory will be preserved until the next update, at
which point it will be deleted.

Do NOT specify this command:

    atomic-rsync -av host:/remote/files /local/

... UNLESS you want the entire /local dir to be swapped out!

See the "rsync" command for its list of options.  You may not use the
--link-dest or --compare-dest options (since this script uses --link-dest
to make the transfer efficient).  Also, the destination directory cannot
be "/".
EOT
    exit $ret;
}
